﻿using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using HandyControl.Data;
using HandyControl.Expression.Drawing;

namespace HandyControl.Controls;

public class UniformSpacingPanel : Panel
{
    private Orientation _orientation;

    public UniformSpacingPanel()
    {
        _orientation = Orientation.Horizontal;
    }

    public static readonly DependencyProperty OrientationProperty =
        StackPanel.OrientationProperty.AddOwner(typeof(UniformSpacingPanel),
            new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure, OnOrientationChanged));

    private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var p = (UniformSpacingPanel) d;
        p._orientation = (Orientation) e.NewValue;
    }

    public Orientation Orientation
    {
        get => (Orientation) GetValue(OrientationProperty);
        set => SetValue(OrientationProperty, value);
    }

    public static readonly DependencyProperty ChildWrappingProperty = DependencyProperty.Register(
        nameof(ChildWrapping), typeof(VisualWrapping), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(default(VisualWrapping), FrameworkPropertyMetadataOptions.AffectsMeasure));

    public VisualWrapping ChildWrapping
    {
        get => (VisualWrapping) GetValue(ChildWrappingProperty);
        set => SetValue(ChildWrappingProperty, value);
    }

    public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register(
        nameof(Spacing), typeof(double), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure), IsSpacingValid);

    public double Spacing
    {
        get => (double) GetValue(SpacingProperty);
        set => SetValue(SpacingProperty, value);
    }

    public static readonly DependencyProperty HorizontalSpacingProperty = DependencyProperty.Register(
        nameof(HorizontalSpacing), typeof(double), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure), IsSpacingValid);

    public double HorizontalSpacing
    {
        get => (double) GetValue(HorizontalSpacingProperty);
        set => SetValue(HorizontalSpacingProperty, value);
    }

    public static readonly DependencyProperty VerticalSpacingProperty = DependencyProperty.Register(
        nameof(VerticalSpacing), typeof(double), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure), IsSpacingValid);

    public double VerticalSpacing
    {
        get => (double) GetValue(VerticalSpacingProperty);
        set => SetValue(VerticalSpacingProperty, value);
    }

    public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register(
        nameof(ItemWidth), typeof(double), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure),
        IsWidthHeightValid);

    [TypeConverter(typeof(LengthConverter))]
    public double ItemWidth
    {
        get => (double) GetValue(ItemWidthProperty);
        set => SetValue(ItemWidthProperty, value);
    }

    public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register(
        nameof(ItemHeight), typeof(double), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure),
        IsWidthHeightValid);

    [TypeConverter(typeof(LengthConverter))]
    public double ItemHeight
    {
        get => (double) GetValue(ItemHeightProperty);
        set => SetValue(ItemHeightProperty, value);
    }

    public static readonly DependencyProperty ItemHorizontalAlignmentProperty = DependencyProperty.Register(
        nameof(ItemHorizontalAlignment), typeof(HorizontalAlignment?), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(HorizontalAlignment.Stretch, FrameworkPropertyMetadataOptions.AffectsMeasure));

    public HorizontalAlignment? ItemHorizontalAlignment
    {
        get => (HorizontalAlignment?) GetValue(ItemHorizontalAlignmentProperty);
        set => SetValue(ItemHorizontalAlignmentProperty, value);
    }

    public static readonly DependencyProperty ItemVerticalAlignmentProperty = DependencyProperty.Register(
        nameof(ItemVerticalAlignment), typeof(VerticalAlignment?), typeof(UniformSpacingPanel),
        new FrameworkPropertyMetadata(VerticalAlignment.Stretch, FrameworkPropertyMetadataOptions.AffectsMeasure));

    public VerticalAlignment? ItemVerticalAlignment
    {
        get => (VerticalAlignment?) GetValue(ItemVerticalAlignmentProperty);
        set => SetValue(ItemVerticalAlignmentProperty, value);
    }

    private static bool IsWidthHeightValid(object value)
    {
        var v = (double) value;
        return double.IsNaN(v) || v >= 0.0d && !double.IsPositiveInfinity(v);
    }

    private static bool IsSpacingValid(object value)
    {
        if (value is double spacing)
        {
            return double.IsNaN(spacing) || spacing > 0;
        }

        return false;
    }

    private void ArrangeWrapLine(double v, double lineV, int start, int end, bool useItemU, double itemU, double spacing)
    {
        double u = 0;
        var isHorizontal = _orientation == Orientation.Horizontal;

        var children = InternalChildren;
        for (var i = start; i < end; i++)
        {
            var child = children[i];
            if (child == null) continue;

            var childSize = new PanelUvSize(_orientation, child.DesiredSize);
            var layoutSlotU = useItemU ? itemU : childSize.U;

            child.Arrange(isHorizontal ? new Rect(u, v, layoutSlotU, lineV) : new Rect(v, u, lineV, layoutSlotU));

            if (layoutSlotU > 0)
            {
                u += layoutSlotU + spacing;
            }
        }
    }

    private void ArrangeLine(double lineV, bool useItemU, double itemU, double spacing)
    {
        double u = 0;
        var isHorizontal = _orientation == Orientation.Horizontal;

        var children = InternalChildren;
        for (var i = 0; i < children.Count; i++)
        {
            var child = children[i];
            if (child == null) continue;

            var childSize = new PanelUvSize(_orientation, child.DesiredSize);
            var layoutSlotU = useItemU ? itemU : childSize.U;

            child.Arrange(isHorizontal ? new Rect(u, 0, layoutSlotU, lineV) : new Rect(0, u, lineV, layoutSlotU));

            if (layoutSlotU > 0)
            {
                u += layoutSlotU + spacing;
            }
        }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var curLineSize = new PanelUvSize(_orientation);
        var panelSize = new PanelUvSize(_orientation);
        var uvConstraint = new PanelUvSize(_orientation, constraint);
        var itemWidth = ItemWidth;
        var itemHeight = ItemHeight;
        var itemWidthSet = !double.IsNaN(itemWidth);
        var itemHeightSet = !double.IsNaN(itemHeight);
        var childWrapping = ChildWrapping;
        var itemHorizontalAlignment = ItemHorizontalAlignment;
        var itemVerticalAlignment = ItemVerticalAlignment;
        var itemHorizontalAlignmentSet = itemHorizontalAlignment != null;
        var itemVerticalAlignmentSet = ItemVerticalAlignment != null;
        var spacingSize = GetSpacingSize();

        var childConstraint = new Size(
            itemWidthSet ? itemWidth : constraint.Width,
            itemHeightSet ? itemHeight : constraint.Height);

        var children = InternalChildren;
        var isFirst = true;

        if (childWrapping == VisualWrapping.Wrap)
        {
            for (int i = 0, count = children.Count; i < count; i++)
            {
                var child = children[i];
                if (child == null) continue;

                if (itemHorizontalAlignmentSet)
                {
                    child.SetCurrentValue(HorizontalAlignmentProperty, itemHorizontalAlignment);
                }

                if (itemVerticalAlignmentSet)
                {
                    child.SetCurrentValue(VerticalAlignmentProperty, itemVerticalAlignment);
                }

                child.Measure(childConstraint);

                var sz = new PanelUvSize(
                    _orientation,
                    itemWidthSet ? itemWidth : child.DesiredSize.Width,
                    itemHeightSet ? itemHeight : child.DesiredSize.Height);

                if (MathHelper.GreaterThan(curLineSize.U + sz.U + spacingSize.U, uvConstraint.U))
                {
                    panelSize.U = Math.Max(curLineSize.U, panelSize.U);
                    panelSize.V += curLineSize.V + spacingSize.V;
                    curLineSize = sz;

                    if (MathHelper.GreaterThan(sz.U, uvConstraint.U))
                    {
                        panelSize.U = Math.Max(sz.U, panelSize.U);
                        panelSize.V += sz.V + spacingSize.V;
                        curLineSize = new PanelUvSize(_orientation);
                    }
                }
                else
                {
                    curLineSize.U += isFirst ? sz.U : sz.U + spacingSize.U;
                    curLineSize.V = Math.Max(sz.V, curLineSize.V);

                    isFirst = false;
                }
            }
        }
        else
        {
            var layoutSlotSize = constraint;

            if (_orientation == Orientation.Horizontal)
            {
                layoutSlotSize.Width = double.PositiveInfinity;
            }
            else
            {
                layoutSlotSize.Height = double.PositiveInfinity;
            }

            for (int i = 0, count = children.Count; i < count; ++i)
            {
                var child = children[i];
                if (child == null) continue;

                if (itemHorizontalAlignmentSet)
                {
                    child.SetCurrentValue(HorizontalAlignmentProperty, itemHorizontalAlignment);
                }

                if (itemVerticalAlignmentSet)
                {
                    child.SetCurrentValue(VerticalAlignmentProperty, itemVerticalAlignment);
                }

                child.Measure(layoutSlotSize);

                var sz = new PanelUvSize(
                    _orientation,
                    itemWidthSet ? itemWidth : child.DesiredSize.Width,
                    itemHeightSet ? itemHeight : child.DesiredSize.Height);

                curLineSize.U += isFirst ? sz.U : sz.U + spacingSize.U;
                curLineSize.V = Math.Max(sz.V, curLineSize.V);

                isFirst = false;
            }
        }

        panelSize.U = Math.Max(curLineSize.U, panelSize.U);
        panelSize.V += curLineSize.V;

        return new Size(panelSize.Width, panelSize.Height);
    }

    private PanelUvSize GetSpacingSize()
    {
        var spacing = Spacing;

        if (!double.IsNaN(spacing))
        {
            return new PanelUvSize(_orientation, spacing, spacing);
        }

        var horizontalSpacing = HorizontalSpacing;
        if (double.IsNaN(horizontalSpacing))
        {
            horizontalSpacing = 0;
        }

        var verticalSpacing = VerticalSpacing;
        if (double.IsNaN(verticalSpacing))
        {
            verticalSpacing = 0;
        }

        return new PanelUvSize(_orientation, horizontalSpacing, verticalSpacing);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        var firstInLine = 0;
        var itemWidth = ItemWidth;
        var itemHeight = ItemHeight;
        double accumulatedV = 0;
        var itemU = _orientation == Orientation.Horizontal ? itemWidth : itemHeight;
        var curLineSize = new PanelUvSize(_orientation);
        var uvFinalSize = new PanelUvSize(_orientation, finalSize);
        var itemWidthSet = !double.IsNaN(itemWidth);
        var itemHeightSet = !double.IsNaN(itemHeight);
        var useItemU = _orientation == Orientation.Horizontal ? itemWidthSet : itemHeightSet;
        var childWrapping = ChildWrapping;
        var spacingSize = GetSpacingSize();

        var children = InternalChildren;
        var isFirst = true;

        if (childWrapping == VisualWrapping.Wrap)
        {
            for (int i = 0, count = children.Count; i < count; i++)
            {
                var child = children[i];
                if (child == null) continue;

                var sz = new PanelUvSize(
                    _orientation,
                    itemWidthSet ? itemWidth : child.DesiredSize.Width,
                    itemHeightSet ? itemHeight : child.DesiredSize.Height);

                if (MathHelper.GreaterThan(curLineSize.U + (isFirst ? sz.U : sz.U + spacingSize.U), uvFinalSize.U))
                {
                    ArrangeWrapLine(accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU, spacingSize.U);

                    accumulatedV += curLineSize.V + spacingSize.V;
                    curLineSize = sz;

                    firstInLine = i;
                }
                else
                {
                    curLineSize.U += isFirst ? sz.U : sz.U + spacingSize.U;
                    curLineSize.V = Math.Max(sz.V, curLineSize.V);
                }

                isFirst = false;
            }

            if (firstInLine < children.Count)
            {
                ArrangeWrapLine(accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU, spacingSize.U);
            }
        }
        else
        {
            ArrangeLine(uvFinalSize.V, useItemU, itemU, spacingSize.U);
        }

        return finalSize;
    }
}
